package nf.graph.persistence;

import java.io.*;
import java.util.*;
import java.util.regex.Pattern;
import java.lang.reflect.*;
import nf.util.Rational;
import nf.graph.Graph;
import nf.graph.Group;
import nf.graph.Node;
import nf.graph.PropertyBag;

public class GraphReader
{
	private Parser parser;
	private Map<String, Node> nodes;
	
	private static final Pattern IDENTIFIER =
		Pattern.compile("\\b[a-zA-Z_]\\w*\\b");
	private static final Pattern NODE_NAME = Pattern.compile("\\b\\w+\\b");
	private static final Pattern NODE_NAME_RANGE =
		Pattern.compile("\\b((\\w+(\\-\\w+)?)|" +
			"\\[(\\s*|(\\s*\\w+(\\s*,\\s*\\w+)*\\s*))\\])\\b");
	
	private static final Pattern OPEN_PAREN = Pattern.compile("\\(");
	private static final Pattern CLOSE_PAREN = Pattern.compile("\\)");
	
	private static final Pattern OPEN_BRACE = Pattern.compile("\\{");
	private static final Pattern CLOSE_BRACE = Pattern.compile("\\}");
	
	private static final Pattern DOUBLE_QUOTED_STRING =
		Pattern.compile("\"([^\"\\\\]*(\\.[^\"\\\\]*)*)\"");
	private static final Pattern NUMBER =
		Pattern.compile("(\\-?\\d+((\\.\\d+)|(/\\d+))?)\\b");
	
	public GraphReader(File definitionFile) throws FileNotFoundException
	{
		this(new Parser(definitionFile));
	}
	
	public GraphReader(String definition)
	{
		this(new Parser(definition));
	}
	
	public GraphReader(Reader definitionReader)
	{
		this(new Parser(definitionReader));
	}
	
	private GraphReader(Parser parser)
	{
		this.parser = parser;
		this.nodes = new HashMap<String, Node>();
	}
	
	public Graph read() throws GraphReadException
	{
		try {
			return parse();
		} catch (ParseException e) {
			throw new GraphReadException("Parse error in graph file", e);
		}
	}
	
	private Object getProperty(PropertyBag bag, String propertyName)
		throws GraphReadException
	{
		return PropertyBag.Access.get(bag, propertyName);
	}
	
	private void setProperties(PropertyBag bag, Map<String, ?> props)
		throws GraphReadException
	{
		for (Map.Entry<String, ?> entry : props.entrySet()) {
			PropertyBag.Access.set(bag, entry.getKey(), entry.getValue());
		}
	}
	
	private <T> T instantiate(Class<?> parsedClass, Class<T> desiredClass)
		throws GraphReadException
	{
		try {
			Object objGraph = parsedClass.newInstance();
			return desiredClass.cast(objGraph);
		} catch (ClassCastException e) {
			throw new GraphReadException(parsedClass.getName() + " is not a " +
				"subclass of " + desiredClass.getName() + ".", parser, e);
		} catch (IllegalAccessException e) {
			throw new GraphReadException("Cannot instantiate " +
				parsedClass.getName() + ".", parser, e);
		} catch (InstantiationException e) {
			throw new GraphReadException("Cannot instantiate " +
				parsedClass.getName() + ".", parser, e);
		}
	}
	
	@SuppressWarnings("unchecked")
	private <T> Class<T> castClass(Class<?> raw, Class<T> desired)
		throws GraphReadException
	{
		if (desired.isAssignableFrom(raw)) {
			return (Class<T>) raw;
		} else {
			throw new GraphReadException(String.format("Class %s is not a " +
				"subclass of %s.", raw.getName(), desired.getName()), parser);
		}
	}
	
	private Graph parse() throws GraphReadException
	{
		String initial = parser.expect("\\b(graph|group)\\b");
		Graph graph;
		Class<?> groupClass = Group.class;
		Object specifiedGroupClass;
		
		if (initial.equals("graph")) {
			graph = parseGraphDeclaration();
			initial = parser.expect("group");
		} else {
			graph = new Graph();
		}
		
		specifiedGroupClass = getProperty(graph, "groupClass");
		if (specifiedGroupClass instanceof Class) {
			groupClass = (Class<?>) specifiedGroupClass;
		}
		
		parseGroup(graph, null, groupClass, Node.class);
		return graph;
	}
	
	private Graph parseGraphDeclaration() throws GraphReadException
	{
		Graph graph;
		
		if (parser.peek(OPEN_PAREN) != null) {
			parser.expect(OPEN_PAREN);
			graph = instantiate(parseClass(), Graph.class);
			parser.expect(CLOSE_PAREN);
		} else {
			graph = new Graph();
		}
		
		if (parser.peek(OPEN_BRACE) != null) {
			setProperties(graph, parseParameters());
		}
		
		return graph;
	}
	
	@SuppressWarnings("unchecked")
	private Group parseGroup(Graph graph, Group parentGroup,
		Class<?> groupClass, Class<? extends Node> nodeClass)
		throws GraphReadException
	{
		Group group = null;
		String name;
		Map<String, ?> params;
		Class<?> rawClass;
		
		if (parser.peek(OPEN_PAREN) != null) {
			parser.expect(OPEN_PAREN);
			group = instantiate(parseClass(), Group.class);
			parser.expect(CLOSE_PAREN);
		} else {
			group = instantiate(groupClass, Group.class);
		}
		
		if (parentGroup == null)
			graph.setRootGroup(group);
		else
			parentGroup.addChildGroup(group);
		
		parser.expect(OPEN_BRACE);
		
		while ((name = parser.get(NODE_NAME_RANGE)) != null) {
			if (name.equals("node")) {
				group.add(parseNode(nodeClass));
			} else if (name.equals("nodes")) {
				group.addAll(parseNodes(nodeClass));
			} else if (name.equals("options")) {
				params = parseParameters();
				
				if (params.containsKey("nodeClass")) {
					rawClass = castClass((Class<?>) params.remove("nodeClass"),
						Node.class);
				}
				if (params.containsKey("groupClass")) {
					groupClass = castClass(
						(Class<?>) params.remove("groupClass"),
						Group.class
					);
				}
				
				setProperties(group, params);
			} else if (name.equals("group")) {
				parseGroup(graph, group, groupClass, nodeClass);
			} else {
				parseEdgeChain(graph, name);
			}
		}
		
		parser.expect(CLOSE_BRACE);
		return group;
	}
	
	private Node parseNode(Class<? extends Node> nodeClass)
		throws GraphReadException
	{
		String name;
		Node node;
		
		if (parser.peek(OPEN_PAREN) != null) {
			parser.expect(OPEN_PAREN);
			node = instantiate(parseClass(), Node.class);
			parser.expect(CLOSE_PAREN);
		} else {
			node = instantiate(nodeClass, Node.class);
		}
		
		name = parser.expect(NODE_NAME);
		node.setName(name);
		nodes.put(name, node);
		
		if (parser.peek(OPEN_BRACE) != null) {
			setProperties(node, parseParameters());
		}
		
		return node;
	}
	
	private List<Node> parseNodes(Class<?> nodeClass)
		throws GraphReadException
	{
		List<String> nodeNames;
		List<Node> myNodes = new LinkedList<Node>();
		Map<String, ?> props;
		
		if (parser.peek(OPEN_PAREN) != null) {
			parser.expect(OPEN_PAREN);
			nodeClass = parseClass();
			parser.expect(CLOSE_PAREN);
		}
		
		nodeNames = parseNodeNames(parser.expect(NODE_NAME_RANGE));
		
		for (String name : nodeNames) {
			Node node = instantiate(nodeClass, Node.class);
			nodes.put(name, node);
			node.setName(name);
			myNodes.add(node);
		}
		
		if (parser.peek(OPEN_BRACE) != null) {
			props = parseParameters();
			for (Node n : myNodes) {
				setProperties(n, props);
			}
		}
		
		return myNodes;
	}
	
	private void parseEdgeChain(Graph graph, String from)
		throws GraphReadException
	{
		String to;
		List<Node> fromNodes, toNodes;
		
		fromNodes = resolveNodeNames(parseNodeNames(from));
		
		while (parser.get("-?>") != null) {
			to = parser.expect(NODE_NAME_RANGE);
			toNodes = resolveNodeNames(parseNodeNames(to));
			
			for (Node fromNode : fromNodes) {
				for (Node toNode : toNodes) {
					if (!fromNode.equals(toNode))
						graph.connect(fromNode, toNode);
				}
			}
			
			fromNodes = toNodes;
		}
	}
	
	private List<Node> resolveNodeNames(List<String> names)
		throws GraphReadException
	{
		List<Node> nodeList = new ArrayList<Node>(names.size());
		
		for (String name : names) {
			Node node = nodes.get(name);
			if (node == null) {
				throw new GraphReadException("No such node '" + name + "'.",
					parser);
			}
			nodeList.add(node);
		}
		
		return nodeList;
	}
	
	private List<String> parseNodeNames(String names)
		throws GraphReadException
	{
		if (names.indexOf("-") >= 0) {
			return parseNodeRange(names);
		} else if (names.charAt(0) == '[') {
			return parseNodeList(names.substring(1, names.length() - 1));
		} else {
			return Collections.singletonList(names);
		}
	}
	
	private List<String> parseNodeList(String nameListExpression)
	{
		String[] names = nameListExpression.split("\\s*,\\s*");
		List<String> nameList = new ArrayList<String>(names.length);
		
		for (String name : names) {
			if (name.length() > 0) {
				nameList.add(name);
			}
		}
		
		return nameList;
	}
	
	private List<String> parseNodeRange(String names)
		throws GraphReadException
	{
		int start, end;
		String[] parts;
		
		class NodeRange extends AbstractList<String>
		{
			private int start;
			private int end;
			
			public NodeRange(int start, int end)
			{
				this.start = start;
				this.end = end;
			}
			
			public String get(int i)
			{
				if (i >= size()) {
					throw new IndexOutOfBoundsException(Integer.toString(i));
				}
				
				return Integer.toString(start + i);
			}
			
			public int size()
			{
				return end - start + 1;
			}
		}
		
		try {
			parts = names.split("-");
			start = Integer.parseInt(parts[0]);
			end = Integer.parseInt(parts[1]);
			return new NodeRange(start, end);
		} catch (NumberFormatException e) {
			throw new ParseException("Invalid integer in node range.",
				parser.getLineNumber(), e);
		}
	}
	
	private Map<String, ?> parseParameters()
	{
		String name;
		Map<String, Object> params = new HashMap<String, Object>();
		
		parser.expect(OPEN_BRACE);
		
		while (null != (name = parser.get(IDENTIFIER))) {
			parser.expect("=");
			params.put(name, parseRValue());
			
			if (parser.get(",") == null)
				break;
		}

		parser.expect(CLOSE_BRACE);
		return params;
	}
	
	private Object parseRValue()
	{
		if (parser.peek("\"") != null)
			return parseString();
		else if (parser.peek("[-\\d]") != null)
			return parseNumber();
		else if (parser.peek("\\w") != null)
			return parseClass();
		else
			throw new ParseException("Invalid rvalue.", parser.getLineNumber());
	}
	
	private String parseString()
	{
		String quotedString = parser.expect(DOUBLE_QUOTED_STRING);
		quotedString = quotedString.substring(1, quotedString.length() - 1);
		
		return quotedString.replaceAll("\\\"", "\"");
	}
	
	private Number parseNumber()
	{
		String numString = parser.expect(NUMBER);
		
		try {
			if (numString.indexOf('.') >= 0) {
				return new Double(numString);
			} else if (numString.indexOf('/') >= 0) {
				return new Rational(numString);
			} else {
				return new Integer(numString);
			}
		} catch (NumberFormatException e) {
			throw new ParseException("Invalid number '" + numString + "'.",
				parser.getLineNumber(), e);
		}
	}
	
	private Class<?> parseClass() 
	{
		String name = parser.expect("\\b\\w+(\\.\\w+)*\\b");
		try {
			return Class.forName(name);
		} catch (ClassNotFoundException e) {
			throw new ParseException("Unknown class '" + name + "'.",
				parser.getLineNumber(), e);
		}
	}
}
